package com.ft.turorial.spring.boot.controller;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.oltu.oauth2.as.issuer.MD5Generator;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl;
import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest;
import org.apache.oltu.oauth2.as.response.OAuthASResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.message.types.ResponseType;
import org.apache.oltu.oauth2.common.utils.OAuthUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import com.ft.turorial.spring.boot.security.SecurityConstants;
import com.ft.turorial.spring.boot.security.oauth2.service.OAuth2ClientService;
import com.ft.turorial.spring.boot.security.oauth2.service.OAuth2Service;
import com.ft.turorial.spring.boot.security.shiro.authc.UsernamePasswordCaptchaToken;
import com.ft.turorial.spring.boot.security.shiro.exception.CaptchaException;
import com.ft.turorial.spring.boot.security.util.DeviceUtil;

/**
 * 支持应用的OAuth2认证(应用登陆时跳转到此Controller来认证:http://localhost:8088/authorize)
 * 
 * 	     代替原SecurityController功能，SecurityController用于OAuth2认证服务器的本地登录
 * 认证流程：
 * 1.客户端(应用程序：http://localhost:8080/login)在其本地未认证通过时跳转到：http://localhost:8088/authorize
?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:8080/chapter17-client/oauth2-login
 * @author lc
 *
 */
@Controller
public class AuthorizeController {
	private  Logger logger = LoggerFactory.getLogger(getClass());
	
	@Resource
    private OAuth2Service oAuthService;
    @Resource
    private OAuth2ClientService clientService;
    //用于获取认证缓存信息
  	@Resource
  	private EhCacheManager cacheManager;
  	
    @RequestMapping("/connect/oauth2/authorize")
    public Object authorize(Model model, HttpServletRequest request)throws URISyntaxException, OAuthSystemException {

        try {
            //构建OAuth 授权请求
            OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);

            //检查传入的客户端id是否正确
            if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
                OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                                .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                                .setErrorDescription(SecurityConstants.OAUTH2_INVALID_CLIENT_DESCRIPTION)
                                .buildJSONMessage();
                return new ResponseEntity<String>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }
            //获取到客户端重定向的地址
            String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
            Subject subject = SecurityUtils.getSubject();
            //如果用户没有登录，跳转到登陆页面，并携带回调地址(即返回上一页面)
            if(!subject.isAuthenticated()) {
                if(!login(subject, request)) {//登录失败时跳转到登陆页面
                    model.addAttribute("client_id", oauthRequest.getClientId());
                    model.addAttribute("redirect_uri", redirectURI);
                    model.addAttribute("response_type", oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE));
                    //如果此处再次需要转到其他平台登录，比如微信/QQ/Weibo等oauth认证，可以在此将其认证路径也放到model中。
                    
                    return "security/oauth_login";
                }
            }

            String userName = (String)subject.getPrincipal();
            //生成授权码
            String authorizationCode = null;
            //responseType目前仅支持CODE，另外还有TOKEN
            String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
            if (responseType.equals(ResponseType.CODE.toString())) {
                OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
                authorizationCode = oauthIssuerImpl.authorizationCode();
                oAuthService.addAuthCode(authorizationCode, userName);
            }
            //重定向到访问的应用的url,并携带授权码
    		return redirectToClient(request,authorizationCode,redirectURI);
        } catch (OAuthProblemException e) {
            //出错处理
            String redirectUri = e.getRedirectUri();
            if (OAuthUtils.isEmpty(redirectUri)) {
                //告诉客户端没有传入redirectUri直接报错
                return new ResponseEntity<String>("OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
            }
            //返回错误消息（如?error=）
            final OAuthResponse response =
                    OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
                            .error(e).location(redirectUri).buildQueryMessage();
            HttpHeaders headers = new HttpHeaders();
            headers.setLocation(new URI(response.getLocationUri()));
            return new ResponseEntity<String>(headers, HttpStatus.valueOf(response.getResponseStatus()));
        }
    }
    
    /**
     * 认证成功后 重定向到client(应用中)
     * @param request
     * @param authorizationCode
     * @param redirectURI 格式：http://xxx:xx/../xx?code=xxxxxxxxxxxxxx
     * @return
     */
    private Object redirectToClient(HttpServletRequest request, String authorizationCode, String redirectURI){
    	try {
			// 构建ResponseEntity响应头
			HttpHeaders headers = new HttpHeaders();
			// 进行OAuth响应构建
			OAuthASResponse.OAuthAuthorizationResponseBuilder builder = 
					OAuthASResponse.authorizationResponse(request,HttpServletResponse.SC_FOUND);
			// 设置授权码
			builder.setCode(authorizationCode);
			// 构建响应
			final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
			// 根据OAuthResponse返回ResponseEntity响应
			headers.setLocation(new URI(response.getLocationUri()));
			logger.info("method redirectToClient redirectURI:{}",redirectURI);
			return new ResponseEntity<String>(headers, HttpStatus.valueOf(response.getResponseStatus()));
		} catch (OAuthSystemException e) {
			e.printStackTrace();
		} catch (URISyntaxException e) {
			e.printStackTrace();
		}
    	return null;
    }
    
    /**
     * 认证本地账户信息
     * @param subject
     * @param request
     * @return
     */
    private boolean login(Subject subject, HttpServletRequest request) {
        if("get".equalsIgnoreCase(request.getMethod())) {
            return false;
        }
        String userName = request.getParameter("userName");
        String password = request.getParameter("password");
        
        if(StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
            return false;
        }
        UsernamePasswordCaptchaToken token;
        //如果是移动设备不验证验证码
  		if( DeviceUtil.isMobile(request)) {
  			logger.debug("移动设备验证，验证码。");
  			token =  new UsernamePasswordCaptchaToken(userName, password);
  	  	}else{
  			token =  new UsernamePasswordCaptchaToken(userName, password,request.getParameter("captcha"));
  		}

        try {  
            logger.info("对用户[" + userName + "]进行登录验证..验证开始");  
            subject.login(token);  
            logger.info("对用户[" + userName + "]进行登录验证..验证通过");  
            return true;
        }catch(CaptchaException ce){  
            logger.info("对用户[" + userName + "]进行登录验证..验证码错误"); 
            request.setAttribute("message", "验证码错误");
        }catch(UnknownAccountException uae){  
            logger.info("对用户[" + userName + "]进行登录验证..验证未通过,未知账户");  
            request.setAttribute("message", "未知账户");
        }catch(IncorrectCredentialsException ice){  
            logger.info("对用户[" + userName + "]进行登录验证..验证未通过,错误的凭证");
            Cache<String, AtomicInteger> passwordRetryCache = cacheManager.getCache("passwordRetryCache");
            request.setAttribute("message", "密码不正确(错误次数："+passwordRetryCache.get(userName)+"次)");
        }catch(LockedAccountException lae){  
            logger.info("对用户[" + userName + "]进行登录验证..验证未通过,账户已锁定");
            request.setAttribute("message", "账户已锁定");
        }catch(ExcessiveAttemptsException eae){  
            logger.info("对用户[" + userName + "]进行登录验证..验证未通过,错误次数过多");  
            request.setAttribute("message", "用户名或密码错误次数过多");
        }catch(AuthenticationException ae){  
            //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景  
            logger.info("对用户[" + userName + "]进行登录验证..验证未通过,堆栈轨迹如下");  
            ae.printStackTrace();  
            request.setAttribute("message", "用户名或密码不正确");
        }
		return false;  
    }
}